Add as_object Liquid filter

The `as_object` returns the received data/object as is without casting it to a string like liquid normally does. It
can be used as a JSONPath replacement or to emit result of a Liquid filter chain as an array.

`catch` and `throw` needs to be used to break out of Liquid render chain. Liquid aggregates the output of every
expression an array and [joins](https://github.com/Shopify/liquid/blob/v3.0.6/lib/liquid/block.rb#L147) it together that
join makes it impossible to get anything else than a string out of a Liquid template.

Dominik Sander 7 years ago
parent
commit
d2cbd04ac8

+ 11 - 6
app/concerns/liquid_droppable.rb

@@ -18,6 +18,11 @@ module LiquidDroppable
18 18
         yield [name, __send__(name)]
19 19
       }
20 20
     end
21
+
22
+    def as_json
23
+      return {} unless defined?(self.class::METHODS)
24
+      Hash[self.class::METHODS.map { |m| [m, send(m).as_json]}]
25
+    end
21 26
   end
22 27
 
23 28
   included do
@@ -33,12 +38,10 @@ module LiquidDroppable
33 38
     self.class::Drop.new(self)
34 39
   end
35 40
 
36
-  class MatchDataDrop < Liquid::Drop
37
-    def initialize(object)
38
-      @object = object
39
-    end
41
+  class MatchDataDrop < Drop
42
+    METHODS = %w[pre_match post_match names size]
40 43
 
41
-    %w[pre_match post_match names size].each { |attr|
44
+    METHODS.each { |attr|
42 45
       define_method(attr) {
43 46
         @object.__send__(attr)
44 47
       }
@@ -64,7 +67,9 @@ module LiquidDroppable
64 67
   require 'uri'
65 68
 
66 69
   class URIDrop < Drop
67
-    URI::Generic::COMPONENT.each { |attr|
70
+    METHODS = URI::Generic::COMPONENT
71
+
72
+    METHODS.each { |attr|
68 73
       define_method(attr) {
69 74
         @object.__send__(attr)
70 75
       }

+ 22 - 1
app/concerns/liquid_interpolatable.rb

@@ -92,7 +92,9 @@ module LiquidInterpolatable
92 92
 
93 93
   def interpolate_string(string, self_object = nil)
94 94
     interpolate_with(self_object) do
95
-      Liquid::Template.parse(string).render!(interpolation_context)
95
+      catch :as_object do
96
+        Liquid::Template.parse(string).render!(interpolation_context)
97
+      end
96 98
     end
97 99
   end
98 100
 
@@ -225,6 +227,25 @@ module LiquidInterpolatable
225 227
       JSON.dump(input)
226 228
     end
227 229
 
230
+    # Returns a Ruby object
231
+    #
232
+    # It can be used as a JSONPath replacement for Agents that only support Liquid:
233
+    #
234
+    # Event:   {"something": {"nested": {"data": 1}}}
235
+    # Liquid:  {{something.nested | as_object}}
236
+    # Returns: {"data": 1}
237
+    #
238
+    # Splitting up a string with Liquid filters and return the Array:
239
+    #
240
+    # Event:   {"data": "A,B,C"}}
241
+    # Liquid:  {{data | split: ',' | as_object}}
242
+    # Returns: ['A', 'B', 'C']
243
+    #
244
+    # as_object ALWAYS has be the last filter in a Liquid expression!
245
+    def as_object(object)
246
+      throw :as_object, object.as_json
247
+    end
248
+
228 249
     private
229 250
 
230 251
     def logger

+ 4 - 2
app/models/agent.rb

@@ -443,7 +443,7 @@ class AgentDrop
443 443
     @object.short_type
444 444
   end
445 445
 
446
-  [
446
+  METHODS = [
447 447
     :name,
448 448
     :type,
449 449
     :options,
@@ -456,7 +456,9 @@ class AgentDrop
456 456
     :disabled,
457 457
     :keep_events_for,
458 458
     :propagate_immediately,
459
-  ].each { |attr|
459
+  ]
460
+
461
+  METHODS.each { |attr|
460 462
     define_method(attr) {
461 463
       @object.__send__(attr)
462 464
     } unless method_defined?(attr)

+ 4 - 0
app/models/event.rb

@@ -119,4 +119,8 @@ class EventDrop
119 119
   def _location_
120 120
     @object.location
121 121
   end
122
+
123
+  def as_json
124
+    {location: _location_.as_json, agent: @object.agent.to_liquid.as_json, payload: @payload.as_json, created_at: created_at.as_json}
125
+  end
122 126
 end

+ 55 - 0
spec/concerns/liquid_interpolatable_spec.rb

@@ -264,4 +264,59 @@ describe LiquidInterpolatable::Filters do
264 264
       expect(agent.interpolated['cleaned']).to eq('FOObar ZOObar')
265 265
     end
266 266
   end
267
+
268
+  context 'as_object' do
269
+    let(:agent) { Agents::InterpolatableAgent.new(name: "test") }
270
+
271
+    it 'returns an array that was splitted in liquid tags' do
272
+      agent.interpolation_context['something'] = 'test,string,abc'
273
+      agent.options['array'] = "{{something | split: ',' | as_object}}"
274
+      expect(agent.interpolated['array']).to eq(['test', 'string', 'abc'])
275
+    end
276
+
277
+    it 'returns an object that was not modified in liquid' do
278
+      agent.interpolation_context['something'] = {'nested' => {'abc' => 'test'}}
279
+      agent.options['object'] = "{{something.nested | as_object}}"
280
+      expect(agent.interpolated['object']).to eq({"abc" => 'test'})
281
+    end
282
+
283
+    context 'as_json' do
284
+      def ensure_safety(obj)
285
+        JSON.parse(JSON.dump(obj))
286
+      end
287
+
288
+      it 'it converts "complex" objects' do
289
+        agent.interpolation_context['something'] = {'nested' => Service.new}
290
+        agent.options['object'] = "{{something | as_object}}"
291
+        expect(agent.interpolated['object']).to eq({'nested'=> ensure_safety(Service.new.as_json)})
292
+      end
293
+
294
+      it 'works with AgentDrops' do
295
+        agent.interpolation_context['something'] = agent
296
+        agent.options['object'] = "{{something | as_object}}"
297
+        expect(agent.interpolated['object']).to eq(ensure_safety(agent.to_liquid.as_json.stringify_keys))
298
+      end
299
+
300
+      it 'works with EventDrops' do
301
+        event = Event.new(payload: {some: 'payload'}, agent: agent, created_at: Time.now)
302
+        agent.interpolation_context['something'] = event
303
+        agent.options['object'] = "{{something | as_object}}"
304
+        expect(agent.interpolated['object']).to eq(ensure_safety(event.to_liquid.as_json.stringify_keys))
305
+      end
306
+
307
+      it 'works with MatchDataDrops' do
308
+        match = "test string".match(/\A(?<word>\w+)\s(.+?)\z/)
309
+        agent.interpolation_context['something'] = match
310
+        agent.options['object'] = "{{something | as_object}}"
311
+        expect(agent.interpolated['object']).to eq(ensure_safety(match.to_liquid.as_json.stringify_keys))
312
+      end
313
+
314
+      it 'works with URIDrops' do
315
+        uri = URI.parse("https://google.com?q=test")
316
+        agent.interpolation_context['something'] = uri
317
+        agent.options['object'] = "{{something | as_object}}"
318
+        expect(agent.interpolated['object']).to eq(ensure_safety(uri.to_liquid.as_json.stringify_keys))
319
+      end
320
+    end
321
+  end
267 322
 end